js 基础-继承和原型链
2023-07-22 19:41:32 # fontend

1. 如何理解原型和原型链

js 的所有对象都继承自原型对象,每个对象上都有一个私有属性指向另一个原型对象,原型对象上也有一个自己的原型,依此类推,直到一个对象的原型是 null,根据定义 null 没有原型,所以它就是这个原型链的最后一个节点。

2. [] 的原型链是怎样的?

[] > Array > Object > null

3. __proto__ [[Prototype]] prototype 区别是什么

js 中,每个物件都有一个内部属性 [[Prototype]] 来标识物件的原型,但是这个内部属性是不能直接访问到的,浏览器就实现了非标准的访问方法 __proto__, 实际开发中建议通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数来访问。

prototype 表示的是函数 func.prototype, 是存在于构造函数中的一个属性,构造函数的 prototype__proto__ 指向同一个原型对象。

1
2
3
4
5
6
7
8
9
function Person() {}
let personA = new Person();

console.log(Person.prototype.constructor.name); // 'Person'

// 对象的 __proto__ 指向它的原型对象,Person.prototype 也指向构造函数的原型对象
personA.__proto__ === Person.prototype; // true
Object.getPrototypeOf(personA) === Person.prototype; // true
personA.__proto__ === Object.getPrototypeOf(personA); // true

4. js 继承有哪几种方式

1. 原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
function Animal() {}

// 实例
const cat = new Animal();

// 往原型对象加上方法
Animal.prototype.sleep = function () {
console.log('sleep');
};

// 使用构造函式的 prototype 的方法
cat.sleep(); // sleep

主要问题:

  1. 引用类型的属性被所有实例共享
  2. 在创建 cat 实例的时候,无法向 Animal 构造函数中传参

2.构造函数继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Super() {
this.colors = ['green', 'blue', 'red'];
}

function Sub() {
Super.call(this);
}

let sub1 = new Sub();
sub1.colors.push('yellow');
console.log(sub1.colors); // ['green', 'blue', 'red', 'yellow']

let sub2 = new Sub();
console.log(sub2.colors); // ['green', 'blue', 'red']

解决了原型继承中实例共享和无法向父构造函数传参的问题,但是方法都定义在构造函数中,每次创建实例都会创建一次所有方法。

3. 组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Super(name, age) {
this.name = name || 'xiaoming';
this.age = age || 30;
}

function Sub(name, age) {
Super.call(this, name, age); // 第一次调用父构造函数
}

Super.prototype.hello = function() {
return `${this.name} has ${this.age}`;
}

Sub.prototype = new Super(); // 第二次调用
Sub.prototype.constructor = Sub;
let instance = new Sub('xiaohong', 18);
instance.hello(); // 'xiaohong has 18'

组合继承结合了原型链和构造函数继承的优点,函数可复用,可向父构造函数传参,实例属性不共享,但是缺点是父构造函数调用了两次。

4. 寄生组合式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Super(name, age) {
this.name = name || 'xiaoming';
this.age = age || 30;
}

function Sub(name, age) {
Super.call(this, name, age); // 第一次调用父构造函数
}

Super.prototype.hello = function() {
return `${this.name} has ${this.age}`;
}

Sub.prototype = Object.create(Super.prototype); // 基于 Super.prototype 创建新对象并将 Sub 的原型对象指向该对象
Sub.prototype.constructor = Sub;

Sub.prototype.getName = function() {
return this.name;
}

let instance = new Sub('xiaohong', 18);
instance.hello(); // 'xiaohong has 18'
instance.getName(); // 'xiaohong'

该继承方式弥补了组合式继承调用两次父类的问题,也就是我们常用的 es5 继承方法。

5.类继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class People {
constructor(name, age) {
this.name = name;
this.age = age;
}

hello() {
console.log(`${this.name} has ${this.age}`);
}
}

class Woman extends People {
constructor(name, age) {
super(name, age);
}

hello() {
super.hello();
}
}

let girl = new Woman('lili', 18);
girl.hello(); // lili has 18

class 实际上就是 es5 构造函数的语法糖,通过 new 关键字创建实例,通过 extends 实现继承,extends 的作用就是将 Woman.prototype 指向 People. extends 的实现逻辑其实就是来自于寄生组合式继承方法。

es6 通过 babel 编译成 es5,其中实现继承的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _inherits(subClass, superClass) { 
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass)
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}